查看原文
其他

ViewBinding,您真的理解了吗? | 开发者说·DTalk

The following article is from Android开发那点事儿 Author 不说谎的匹诺槽

本文原作者: 不说谎的匹诺槽,原‍文发布于: Android开发那点事儿


Android 开发中,控件绑定是一个久远的话题。

最开始就是使用 findViewById,满屏都是各种 find。

后来出现了 Butterknife,使用注解来进行控件绑定,这样一来使 UI 层的代码清爽了很多,即使这样,还存在众多臃肿的全局变量的控件。

后来 kotlin 推出了 kotlin-android-extensions 插件,可以直接用 id 就能得到 xml 中的控件对象并且使用,这种方式真的很香,它原理是利用了字节码插桩技术,帮我们自动生成了类似 findViewById 的东西。

具体废弃的原因我猜测:

  1. 不兼容 Java。虽然现在 Google 各种新技术都在以 java 为主,像协程,Compose 之类,但是这些都是可以独立于平台的,而控件绑定这个功能是基于平台的,必然需要考虑 java 用户群体;

  2. 类型安全: res 下的任何 id 都可以被访问,有可能因访问了非当前 Layout 下的 id 而出错;

  3. 虽然 kotlin-android-extensions 我们使用起来非常爽,但是从它的实现原理也暴露出来一些问题,在无形之中降低了程序的运行效率。



使用



ViewBinding 的简单使用可以说是非常简单。


首先在我们的 moudlebuild.gradle 下进行配置: 

buildFeatures { viewBinding true}


vViewbinding 的配置是独立于 moudle 的。


配置之后,会生成对应的 Binding 类,我们直接调用,进行绑定即可。

lateinit var binding: MainActivityBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = MainActivityBinding.inflate(layoutInflater) setContentView(binding.root) binding.message.text = "Android开发那点事儿" }


在使用上,我们就可以直接通过 binding 来获取到我们 XML 布局中定义的控件,非常方便。


原理



生成的 binding 文件是在  build/generated/data_binding_base_class_source_out/debug/out 目录下,我们先看下生成的类内容。


这是我定义的 XML 布局:

<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/main" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/message" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="MainFragment" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /></androidx.constraintlayout.widget.ConstraintLayout>


这是生成的 binding 类: 
public final class MainActivityBinding implements ViewBinding { private final ConstraintLayout rootView; public final ConstraintLayout main; public final TextView message; private MainActivityBinding( ConstraintLayout rootView, ConstraintLayout main, TextView message) { this.rootView = rootView; this.main = main; this.message = message; }
@Override @NonNull public ConstraintLayout getRoot() { return rootView; }
@NonNull public static MainActivityBinding inflate(@NonNull LayoutInflater inflater) { return inflate(inflater, null, false); }
@NonNull public static MainActivityBinding inflate(@NonNull LayoutInflater inflater, @Nullable ViewGroup parent, boolean attachToParent) { View root = inflater.inflate(R.layout.main_activity, parent, false); if (attachToParent) { parent.addView(root); } return bind(root); }
@NonNull public static MainActivityBinding bind(@NonNull View rootView) { int id; missingId: { ConstraintLayout main = (ConstraintLayout) rootView;
id = R.id.message; TextView message = ViewBindings.findChildViewById(rootView, id); if (message == null) { break missingId; } return new MainActivityBinding((ConstraintLayout) rootView, main, message); } String missingId = rootView.getResources().getResourceName(id); throw new NullPointerException("Missing required view with ID: ".concat(missingId)); }}


在这里 viewbinding 帮我解析的 xml 布局文件,并对设置 id 的控件自动进行控件绑定,最后我们通过 viewbinding 获取根布局,调用 setContent 方法来添加布局。
@Override public void setContentView(View v) { ensureSubDecor(); ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); contentParent.addView(v); mAppCompatWindowCallback.getWrapped().onContentChanged(); }


原来是由 actiivty 进行布局解析,由我们自己进行控件绑定并使用,现在就相当于 ViewBinding 将这两件事情都做了。


那这些 binding 类是如何生成的呢?这就是接下来我们要探索的话题。


当我改变布局文件的时候,发现 binding 类文件并不会实时发生改变,需要编译之后 binding 类文件才会进行对应改变,由此推断,binding 类文件应该是 APG 在编译项目的时候生成的。


我们运行一下,看下 Task

整个编译流程中,只捕捉到了三个关于 dataBindingtask,我们现在并不能确定是哪个 task 生成的 binding 类,那怎么办?

那我们就一个一个执行,一个一个去试。

AS 右侧的 gradle 任务栏中,找到了关于 databindingtask

我们再分别执行 dataBindingMergeDependencyArtifactsDebugdataBindingMergeGenClassesDebugdataBindingGenBaseClassesDebug


clean 操作,然后执行前两个 task 之后,发现只是生成了两个空文件夹,并未有内容生成: 

在执行了 dataBindingMergeGenClassesDebug 之后,生成了我们所需要的 binding 类。

dataBindingMergeGenClassesDebug 这个 task 就是我们要探索的重点。


接下来我们引入 AGP 源码和 Databinding 源码,进行分析: 

implementation 'com.android.tools.build:gradle:7.0.2'implementation 'androidx.databinding:databinding-compiler-common:7.0.2'implementation 'androidx.databinding:databinding-common:7.0.2'implementation 'com.android.databinding:baseLibrary:7.0.2'


appbuild.gradle 中引入就行,我们在 External Libraries 中进行查阅。


我们查找的入口主要有两个,一个 AGPTaskManager 类,这个是管理 Task 创建的类,还有另外一个入口,就是从 /com/android/build/gradle/internal/tasks 路径去找,这个是属于熟能生巧的一个捷径。


首先看 TaskManager,通过方法查阅,我们会发现关于创建 DataBinding Task 的只要一个方法 createDataBindingTasksIfNecessary

protected fun createDataBindingTasksIfNecessary(creationConfig: ComponentCreationConfig) { val dataBindingEnabled = creationConfig.buildFeatures.dataBinding val viewBindingEnabled = creationConfig.buildFeatures.viewBinding if (!dataBindingEnabled && !viewBindingEnabled) { return } taskFactory.register(DataBindingMergeBaseClassLogTask.CreationAction(creationConfig)) taskFactory.register( DataBindingMergeDependencyArtifactsTask.CreationAction(creationConfig)) DataBindingBuilder.setDebugLogEnabled(logger.isDebugEnabled) taskFactory.register(DataBindingGenBaseClassesTask.CreationAction(creationConfig))
// DATA_BINDING_TRIGGER artifact is created for data binding only (not view binding) if (dataBindingEnabled) { if (projectOptions[BooleanOption.NON_TRANSITIVE_R_CLASS] && isKotlinKaptPluginApplied(project)) { // TODO(183423660): Undo this workaround for KAPT resolving files at compile time taskFactory.register(MergeRFilesForDataBindingTask.CreationAction(creationConfig)) } taskFactory.register(DataBindingTriggerTask.CreationAction(creationConfig)) setDataBindingAnnotationProcessorParams(creationConfig) } }


这里首先获取配置信息,看看 ViewBindingDataBinding 的开关状态,如果两个都是关闭直接返回。否则进行 Task 注册,在这里进行注册的 task,有两个是我们在编译过程中看到的 task,再继续,就是当 databinding 开启的时候,会再额外注册 task,通过这里我们可以了解到 viewBinding 只是 DataBinding 中的一部分功能。viewbinding 只是进行控件绑定,DataBinding 除了基础的控件绑定之外,还拥有双向数据绑定等功能。


接下来我们看看 DataBindingGenBaseClassesTask


这个 Task 类的路径是 com.android.build.gradle.internal.tasks.databinding,跟我提到的第二个入口吻合,所以以后分析 AGP 源码,可以从这个路径来找对应的 Task,这是一种取巧的方式。


写过自定义插件的朋友都知道,自定义 Task 中需要用注解 @TaskAction 来标识一下 task 的运行入口。

@TaskAction fun writeBaseClasses(inputs: IncrementalTaskInputs) { recordTaskAction(analyticsService.get()) { val args = buildInputArgs(inputs) CodeGenerator( args, sourceOutFolder.get().asFile, Logger.getLogger(DataBindingGenBaseClassesTask::class.java), encodeErrors, collectResources()).run() } }


可以看到 writeBaseClasses 方法被 @TaskAction 注解标识,那么这就是我们分析的入口。


这里主要是创建了 CodeGenerator 类,然后执行了 run() 方法。

override fun run() { try { initLogger() BaseDataBinder( LayoutInfoInput(args), if (symbolTables != null) this::getRPackage else null) .generateAll(DataBindingBuilder.GradleFileWriter(sourceOutFolder.absolutePath)) } finally { clearLogger() } }


CodeGenerator:: run() 中,我们看到这里又创建了 BaseDataBinder 类,并运行了 generateAll 方法。
fun generateAll(writer : JavaFileWriter) { .............. layoutBindings.forEach { layoutName, variations -> ........... if (variations.first().isBindingData) { check(input.args.enableDataBinding) { "Data binding is not enabled but found data binding layouts: $variations" } val binderWriter = BaseLayoutBinderWriter(layoutModel, libTypes) javaFile = binderWriter.write() classInfo = binderWriter.generateClassInfo() } else { check(input.args.enableViewBinding) { "View binding is not enabled but found non-data binding layouts: $variations" } val viewBinder = layoutModel.toViewBinder() javaFile = viewBinder.toJavaFile(useLegacyAnnotations = !useAndroidX) classInfo = viewBinder.generatedClassInfo() } ................. }


这里对代码进行了精简,这里首先做了一个判断,判断是否为 DataBinding,很明显我们需要分析的内容在 else 里面。


else 里面,先判断了 viewBinding 是否开启,然后将 BaseLayoutModel 对象转化为了 ViewBinder 对象,接下来执行了 ViewBinder 的拓展方法 toJavaFile,这个方法名的意思就很明显了,是去转化为 Java 文件的。

fun ViewBinder.toJavaFile(useLegacyAnnotations: Boolean = false) = JavaFileGenerator(this, useLegacyAnnotations).create()

这里是创建了 JavaFileGenerator 类,执行 create() 方法。
fun create() = javaFile(binder.generatedTypeName.packageName(), typeSpec()) { addFileComment("Generated by view binder compiler. Do not edit!")}


这就是创建 binding 类的方法了,我们主要看下 typeSpec() 方法:
private fun typeSpec() = classSpec(binder.generatedTypeName) { 增加 public final 修饰 addModifiers(PUBLIC, FINAL) 实现 ViewBinding 接口 addSuperinterface(ClassName.get(viewBindingPackage, "ViewBinding")) 添加 rootView 变量 addField(rootViewField()) 添加 控件 变量 addFields(bindingFields()) 创建 无参构造方法 addMethod(constructor()) 创建 根布局的 get方法 addMethod(rootViewGetter())
if (binder.rootNode is RootNode.Merge) { addMethod(mergeInflate()) } else { 创建一个参数的 inflate 方法 addMethod(oneParamInflate()) 创建三个参数的 inflate 方法 addMethod(threeParamInflate()) } 添加 bind 方法 addMethod(bind())}


使用过 javapoet 的同学可以看出这就是使用 javapoet 来创建 java 文件。经过这些创建流程与我们生成的 viewbinding 类文件对比,可以发现完全吻合。所以我们的 ViewBinding 类文件就是在这里通过 javapoet 生成的。



总结



最后总结一下:


我们通过观察编译流程,得出 dataBindingGenBaseClassesDebug 是生成 binding 类的 task,然后通过 TaskManager 找到对应的 DataBindingGenBaseClassesTask,通过 @TaskAction 注解找到 task 执行的入口,最后调用到 DataBinding 里面 BaseDataBinder 类,在这个过程中,通过 ViewBinder 调用到了 JavaFileGeneratorcreate() 方法,在这里通过 javapoet 生成了我们所使用 Viewbinding 类。


整体调用流程: 

TaskManager
->writeBaseClasses
->CodeGenerator :: run()
->BaseDataBinder::generateAll()
->ViewBinder::toJavaFile()
->JavaFileGenerator:: create()
->typeSpec()
->javapoet

整体流程并不算复杂,大家在阅读后最好还是自己去跟一遍源码,这个亲自跟一遍,自己理解的才算透彻。


理论配合实践,才能真正学会。





长按右侧二维码

查看更多开发者精彩分享




"开发者说·DTalk" 面向中国开发者们征集 Google 移动应用 (apps & games) 相关的产品/技术内容。欢迎大家前来分享您对移动应用的行业洞察或见解、移动开发过程中的心得或新发现、以及应用出海的实战经验总结和相关产品的使用反馈等。我们由衷地希望可以给这些出众的中国开发者们提供更好展现自己、充分发挥自己特长的平台。我们将通过大家的技术内容着重选出优秀案例进行谷歌开发技术专家 (GDE) 的推荐。




 点击屏末 | 阅读原文 | 即刻报名参与 "开发者说·DTalk" 




您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存